﻿using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Cnm.Core;
using Cnm.Core.Caching;
using Cnm.Core.Domain.Localization;
using Cnm.Core.Domain.Seo;
using Cnm.Models;
using Cnm.Mvc.Models;
using Cnm.Mvc.Models.Localization;
using Cnm.Core.Ultities;
using Cnm.Servicers.Seo;

namespace Cnm.Servicers.Localization
{
    /// <summary>
    /// Provides information about localizable entities
    /// </summary>
    public partial class LocalizedEntityService : ILocalizedEntityService
    {
        #region Constants

        /// <summary>
        /// Key for caching
        /// </summary>
        /// <remarks>
        /// {0} : language ID
        /// {1} : entity ID
        /// {2} : locale key group
        /// {3} : locale key
        /// </remarks>
        private const string LOCALIZEDPROPERTY_KEY = "Nop.localizedproperty.value-{0}-{1}-{2}-{3}";
        /// <summary>
        /// Key for caching
        /// </summary>
        private const string LOCALIZEDPROPERTY_ALL_KEY = "Nop.localizedproperty.all";
        /// <summary>
        /// Key pattern to clear cache
        /// </summary>
        private const string LOCALIZEDPROPERTY_PATTERN_KEY = "Nop.localizedproperty.";

        #endregion

        #region Fields

        private readonly CnmDemoEntities dbContext;
        private readonly ICacheManager cacheManager;
        private readonly LocalizationSettings localizationSettings;

        #endregion

        #region Ctor
        
        public LocalizedEntityService()
        {
            this.dbContext = new CnmDemoEntities();
            this.localizationSettings = new LocalizationSettings();
            this.cacheManager = new MemoryCacheManager();
        }

        /// <summary>
        /// Ctor
        /// </summary>
        /// <param name="cacheManager">Cache manager</param>
        /// <param name="localizationSettings">Localization settings</param>
        public LocalizedEntityService(ICacheManager cacheManager,
            LocalizationSettings localizationSettings)
        {
            this.cacheManager = cacheManager;
            this.dbContext = new CnmDemoEntities();
            this.localizationSettings = localizationSettings;
        }

        #endregion

        #region Utilities

        /// <summary>
        /// Gets localized properties
        /// </summary>
        /// <param name="entityId">Entity identifier</param>
        /// <param name="localeKeyGroup">Locale key group</param>
        /// <returns>Localized properties</returns>
        protected virtual IList<LocalizedProperty> GetLocalizedProperties(int entityId, string localeKeyGroup)
        {
            if (entityId == 0 || string.IsNullOrEmpty(localeKeyGroup))
                return new List<LocalizedProperty>();

            return dbContext
                .LocalizedProperties
                .Where(p => p.EntityId == entityId && p.LocaleKeyGroup == localeKeyGroup)
                .OrderBy(p => p.Id)
                .ToList();
        }

        /// <summary>
        /// Gets all cached localized properties
        /// </summary>
        /// <returns>Cached localized properties</returns>
        protected virtual IList<LocalizedPropertyForCaching> GetAllLocalizedPropertiesCached()
        {
            //cache
            string key = string.Format(LOCALIZEDPROPERTY_ALL_KEY);
            return cacheManager.Get(key, () =>
            {
                var query = dbContext.LocalizedProperties;
                var localizedProperties = query.ToList();
                var list = new List<LocalizedPropertyForCaching>();
                foreach (var lp in localizedProperties)
                {
                    var localizedPropertyForCaching = new LocalizedPropertyForCaching()
                    {
                        Id = lp.Id,
                        EntityId = lp.EntityId,
                        LanguageId = lp.LanguageId,
                        LocaleKeyGroup = lp.LocaleKeyGroup,
                        LocaleKey = lp.LocaleKey,
                        LocaleValue = lp.LocaleValue
                    };
                    list.Add(localizedPropertyForCaching);
                }
                return list;
            });
        }

        #endregion

        #region Nested classes

        [Serializable]
        public class LocalizedPropertyForCaching
        {
            public int Id { get; set; }
            public int EntityId { get; set; }
            public int LanguageId { get; set; }
            public string LocaleKeyGroup { get; set; }
            public string LocaleKey { get; set; }
            public string LocaleValue { get; set; }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Deletes a localized property
        /// </summary>
        /// <param name="localizedProperty">Localized property</param>
        public virtual void DeleteLocalizedProperty(LocalizedProperty localizedProperty)
        {
            if (localizedProperty == null)
                throw new ArgumentNullException("localizedProperty");

            dbContext.LocalizedProperties.Remove(localizedProperty);
            //cache
            cacheManager.RemoveByPattern(LOCALIZEDPROPERTY_PATTERN_KEY);
        }

        /// <summary>
        /// Gets a localized property
        /// </summary>
        /// <param name="localizedPropertyId">Localized property identifier</param>
        /// <returns>Localized property</returns>
        public virtual LocalizedProperty GetLocalizedPropertyById(int localizedPropertyId)
        {
            if (localizedPropertyId == 0)
                return null;

            return dbContext.LocalizedProperties.FirstOrDefault(p=>p.Id == localizedPropertyId);
        }

        /// <summary>
        /// Find localized value
        /// </summary>
        /// <param name="languageId">Language identifier</param>
        /// <param name="entityId">Entity identifier</param>
        /// <param name="localeKeyGroup">Locale key group</param>
        /// <param name="localeKey">Locale key</param>
        /// <returns>Found localized value</returns>
        public virtual string GetLocalizedValue(int languageId, int entityId, string localeKeyGroup, string localeKey)
        {
            if (localizationSettings.LoadAllLocalizedPropertiesOnStartup)
            {
                string key = string.Format(LOCALIZEDPROPERTY_KEY, languageId, entityId, localeKeyGroup, localeKey);
                return cacheManager.Get(key, () =>
                {
                    //load all records (we know they are cached)
                    var source = GetAllLocalizedPropertiesCached();
                    var query = from lp in source
                                where lp.LanguageId == languageId &&
                                lp.EntityId == entityId &&
                                lp.LocaleKeyGroup == localeKeyGroup &&
                                lp.LocaleKey == localeKey
                                select lp.LocaleValue;
                    var localeValue = query.FirstOrDefault();
                    //little hack here. nulls aren't cacheable so set it to ""
                    if (localeValue == null)
                        localeValue = "";
                    return localeValue;
                });

            }
            else
            {
                //gradual loading
                string key = string.Format(LOCALIZEDPROPERTY_KEY, languageId, entityId, localeKeyGroup, localeKey);
                return cacheManager.Get(key, () =>
                {
                    var localeValue = dbContext
                        .LocalizedProperties
                        .Where(p => p.LanguageId == languageId
                                    && p.EntityId == entityId
                                    && p.LocaleKeyGroup == localeKeyGroup
                                    && p.LocaleKey == localeKey)
                        .Select(p => p.LocaleValue)
                        .FirstOrDefault();
                    //little hack here. nulls aren't cacheable so set it to ""
                    if (localeValue == null)
                        localeValue = "";
                    return localeValue;
                });
            }
        }

        /// <summary>
        /// Inserts a localized property
        /// </summary>
        /// <param name="localizedProperty">Localized property</param>
        public virtual void InsertLocalizedProperty(LocalizedProperty localizedProperty)
        {
            if (localizedProperty == null)
                throw new ArgumentNullException("localizedProperty");
            dbContext.LocalizedProperties.Add(localizedProperty);
            dbContext.SaveChanges();

            //cache
            cacheManager.RemoveByPattern(LOCALIZEDPROPERTY_PATTERN_KEY);
        }

        /// <summary>
        /// Updates the localized property
        /// </summary>
        /// <param name="localizedProperty">Localized property</param>
        public virtual void UpdateLocalizedProperty(LocalizedProperty localizedProperty)
        {
            if (localizedProperty == null)
                throw new ArgumentNullException("localizedProperty");
            dbContext.LocalizedProperties.AddOrUpdate(localizedProperty);

            //cache
            cacheManager.RemoveByPattern(LOCALIZEDPROPERTY_PATTERN_KEY);
        }

        /// <summary>
        /// Save localized value
        /// </summary>
        /// <typeparam name="T">Type</typeparam>
        /// <param name="entity">Entity</param>
        /// <param name="keySelector">Key selector</param>
        /// <param name="localeValue">Locale value</param>
        /// <param name="languageId">Language ID</param>
        public virtual void SaveLocalizedValue<T>(T entity,
            Expression<Func<T, string>> keySelector,
            string localeValue,
            int languageId) where T : BaseEntity, ILocalizedEntity
        {
            SaveLocalizedValue<T, string>(entity, keySelector, localeValue, languageId);
        }

        public virtual void SaveLocalizedValue<T, TPropType>(T entity,
            Expression<Func<T, TPropType>> keySelector,
            TPropType localeValue,
            int languageId) where T : BaseEntity, ILocalizedEntity
        {
            if (entity == null)
                throw new ArgumentNullException("entity");

            if (languageId == 0)
                throw new ArgumentOutOfRangeException("languageId", "Language ID should not be 0");

            var member = keySelector.Body as MemberExpression;
            if (member == null)
            {
                throw new ArgumentException(string.Format(
                    "Expression '{0}' refers to a method, not a property.",
                    keySelector));
            }

            var propInfo = member.Member as PropertyInfo;
            if (propInfo == null)
            {
                throw new ArgumentException(string.Format(
                       "Expression '{0}' refers to a field, not a property.",
                       keySelector));
            }

            string localeKeyGroup = typeof(T).Name;
            string localeKey = propInfo.Name;

            var props = GetLocalizedProperties(entity.Id, localeKeyGroup);
            var prop = props.FirstOrDefault(lp => lp.LanguageId == languageId &&
                lp.LocaleKey.Equals(localeKey, StringComparison.InvariantCultureIgnoreCase)); //should be culture invariant

            string localeValueStr = CommonHelper.To<string>(localeValue);

            if (prop != null)
            {
                if (string.IsNullOrWhiteSpace(localeValueStr))
                {
                    //delete
                    DeleteLocalizedProperty(prop);
                }
                else
                {
                    //update
                    prop.LocaleValue = localeValueStr;
                    UpdateLocalizedProperty(prop);
                }
            }
            else
            {
                if (!string.IsNullOrWhiteSpace(localeValueStr))
                {
                    //insert
                    prop = new LocalizedProperty()
                    {
                        EntityId = entity.Id,
                        LanguageId = languageId,
                        LocaleKey = localeKey,
                        LocaleKeyGroup = localeKeyGroup,
                        LocaleValue = localeValueStr
                    };
                    InsertLocalizedProperty(prop);
                }
            }
        }

        public void SaveLocals<T, TLocalized>(T entity, IList<TLocalized> locales) where T : BaseEntity, ILocalizedEntity
        {
            if (entity == null || locales == null)
                return;
            
            foreach (var localizedModelLocal in locales)
            {
                var localizedModel = localizedModelLocal as ILocalizedModelLocal;
                if (localizedModel == null)
                    return;
                
                if (localizedModel.LanguageId == 0)
                    throw new ArgumentOutOfRangeException("languageId", "Language ID should not be 0");

                var properties = localizedModel.GetType().GetProperties();

                #region Save Localize Property

                foreach (var propInfo in properties)
                {
                    if (propInfo.Name == localizedModel.GetPropertyName(p => p.LanguageId))
                        //|| propInfo.Name == localizedModel.GetPropertyName(p => p.SeName))
                        continue;
                    
                    string localeKeyGroup = typeof(T).Name;
                    string localeKey = propInfo.Name;
                    var localeValue = propInfo.GetValue(localizedModel);

                    var props = GetLocalizedProperties(entity.Id, localeKeyGroup);
                    var prop = props.FirstOrDefault(lp => lp.LanguageId == localizedModel.LanguageId 
                        && lp.LocaleKey.Equals(localeKey, StringComparison.InvariantCultureIgnoreCase)); //should be culture invariant

                    string localeValueStr = CommonHelper.To<string>(localeValue);

                    if (prop != null)
                    {
                        if (string.IsNullOrWhiteSpace(localeValueStr))
                        {
                            //delete
                            DeleteLocalizedProperty(prop);
                        }
                        else
                        {
                            //update
                            prop.LocaleValue = localeValueStr;
                            UpdateLocalizedProperty(prop);
                        }
                    }
                    else
                    {
                        if (!string.IsNullOrWhiteSpace(localeValueStr))
                        {
                            //insert
                            prop = new LocalizedProperty()
                            {
                                EntityId = entity.Id,
                                LanguageId = localizedModel.LanguageId,
                                LocaleKey = localeKey,
                                LocaleKeyGroup = localeKeyGroup,
                                LocaleValue = localeValueStr
                            };
                            InsertLocalizedProperty(prop);
                        }
                    }
                }

                #endregion
            }
        }

        #endregion
    }
}
